# 基于TCP的聊天室程序：客户端
# https://www.cnblogs.com/slfblog/p/5070311.html
import selectors
import queue
import re
import threading
from socket import *
from tkinter import *
from time import ctime

BUFSIZE = 1024

# lock = threading.Lock()    # Global Lock
que = queue.Queue(4096)


class GUI(object):
    '''
    This is the top module. It interacts with users. When a button is clicked
    or Return is pressed, a corresbonding event trigered. Connect is used to
    estanblish a TCP connection with server while Log In tells server a user
    comes. We also provide an Add button, which allows user to add contacts.
    All the contacts will be displayed in a list box, and a double click on
    each contact will direct the user's message to a specific contact.
    Note that this module is based on other modules like Send and Recv, so it
    is not concerned with send and receive details. In fact it is wisdom to
    throw this burden to others. We just do what we can and do it perfectly.
    '''

    def __init__(self):
        self.root = Tk()
        self.root.title('Chat')

        self.frame_lft = Frame(self.root)
        self.frame_rgt = Frame(self.root)
        self.frame_lft.grid(row=0, column=0)
        self.frame_rgt.grid(row=0, column=1)

        # entry, collect input
        self.entry_msg = Entry(self.frame_lft, width=46)
        self.entry_msg.grid(row=1, column=0)
        self.entry_msg.bind('<Return>', self.send_method)

        self.scrollbar_txt = Scrollbar(self.frame_lft, width=1)
        self.scrollbar_txt.grid(row=0, column=2, sticky=W+N+S)

        #self.button_qit = Button(self.root, text='Quit', command=self.root.quit)
        # self.button_qit.pack()

        self.text_msg = Text(self.frame_lft, state=DISABLED,
                             width=49, wrap=WORD)    # text, display message
        self.text_msg.config(font='Fixedsys')
        self.text_msg.grid(row=0, column=0, columnspan=2, sticky=W+N+S+E)
        self.text_msg.config(yscrollcommand=self.scrollbar_txt.set)
        self.scrollbar_txt.config(command=self.text_msg.yview)

        # an IP address is supposed to input here
        self.entry_IP = Entry(self.frame_rgt, width=14)
        self.entry_IP.grid(row=0, column=0, padx=5, pady=0)
        self.entry_IP.bind('<Return>', self.connect_method)

        # click this button to connect
        self.button_cnt = Button(
            self.frame_rgt, text='connect', command=self.connect_method)
        self.button_cnt.config(height=1, width=8)
        self.button_cnt.grid(row=0, column=1, padx=0, pady=0)

        self.button_snd = Button(
            self.frame_lft, height=1, text='send', command=self.send_method)
        self.button_snd.config(width=8)
        self.button_snd.grid(row=1, column=1)

        ## New features in version 2.2 ##
        self.entry_log = Entry(self.frame_rgt, width=14)    # log in
        self.entry_log.grid(row=1, column=0, padx=5, pady=0)
        self.entry_log.bind('<Return>', self.login_method)

        # first click means log in, second means log out
        self.button_log = Button(
            self.frame_rgt, text='log in', command=self.login_method)
        self.button_log.config(width=8)
        self.button_log.grid(row=1, column=1, sticky=W)

        self.listbox_cat = Listbox(self.frame_rgt, height=17, width=24)
        self.listbox_cat.insert(END, '000000')
        self.listbox_cat.grid(
            row=3, column=0, columnspan=2, padx=5, sticky=N+S+E)
        self.listbox_cat.bind('<Double-1>', self.contact_method)

        self.scrollbar_cat = Scrollbar(self.frame_rgt, width=1)
        self.scrollbar_cat.grid(row=3, column=2, sticky=W+N+S)
        self.scrollbar_cat.config(command=self.listbox_cat.yview)
        self.listbox_cat.config(yscrollcommand=self.scrollbar_cat.set)

        self.entry_add = Entry(self.frame_rgt, width=14)
        self.entry_add.grid(row=4, column=0, padx=5, pady=0)
        self.entry_add.bind('<Return>', self.add_method)

        self.button_add = Button(self.frame_rgt, width=8, text='add')
        self.button_add.config(command=self.add_method)
        self.button_add.grid(row=4, column=1)

        self.label_cat = Label(self.frame_rgt, text='Contacts')
        self.label_cat.grid(row=2, column=0, sticky=W)

    def send_method(self, ev=None):
        data = self.entry_msg.get()
        self.entry_msg.delete(0, END)
        if not data:
            pass
        else:
            self.text_msg.config(state=NORMAL)
            self.text_msg.insert(END, data+'\n')
            self.text_msg.config(state=DISABLED)
            self.text_msg.see(END)
            self.send.send(data)

    def recv_method(self):
        try:
            data = que.get(block=False)
        except:
            pass
        else:
            self.text_msg.config(state=NORMAL)
            self.text_msg.insert(END, data+'\n')
            self.text_msg.config(state=DISABLED)
            self.text_msg.see(END)
            if re.match(r'^FROME', data):    # log in failed
                self.entry_log.config(state=NORMAL)
                self.button_log.config(
                    text='log in', command=self.login_method)

        self.root.after(200, self.recv_method)    # runs every 200ms

    def connect_method(self, ev=None):
        IP = self.entry_IP.get()
        self.connt = Connt(IP)    # make an instance of Connt class
        self.connt()              # establish connection
        self.button_cnt.config(state=DISABLED)
        self.entry_IP.config(state=DISABLED)
        # make an instance of Send class
        self.send = Send(self.connt.tcpCliSock)
        # make an instance of Recv class
        self.recv = Recv(self.connt.tcpCliSock)
        # a new thread, dealing with receiving
        self.recv_thread = threading.Thread(target=self.recv)
        self.recv_thread.daemon = True
        self.recv_thread.start()
        self.root.after(200, self.recv_method)

    def login_method(self, ev=None):
        ID = self.entry_log.get()
        if re.match(r'^[0-9]{6}$', ID) == None:
            pass
        else:
            self.send.send(ID)        # this action is infalliable
            self.button_log.config(text='log out', command=self.logout_method)
            self.entry_log.config(state=DISABLED)

    def logout_method(self, ev=None):
        self.send.send('::LOG OUT')
        self.button_log.config(text='log in', command=self.login_method)
        self.entry_log.config(state=NORMAL)

    def contact_method(self, ev=None):
        ID = self.listbox_cat.get(self.listbox_cat.curselection())
        self.send.send('::'+ID)
        self.text_msg.delete(1.0, END)     # delete all text
        self.text_msg.config(state=NORMAL)
        self.text_msg.insert(END, '[to '+ID+' '+ctime()+']\n')
        # if this contact action fails, server will send an error message.

    def add_method(self, ev=None):
        ID = self.entry_add.get()
        if re.match(r'[0-9]{6}', ID) == None:
            pass
        else:
            self.listbox_cat.insert(END, ID)


class Send(object):
    '''
    This module deals with every detail in sending bytes through a socket,
    such as lock, encode, blocking, etc, and provide a send interface for
    GUI module.
    '''

    def __init__(self, fd):
        self.fd = fd
        self.sel = selectors.DefaultSelector()
        self.sel.register(self.fd, selectors.EVENT_WRITE)

    def send(self, data):
        self.sel.select()    # wait until the socket is ready to write
        # if lock.acquire():
        self.fd.send(data.encode('utf-8'))
        # lock.release()
        # else:
        #    pass


class Recv(object):
    '''
    This module deals with every detail in receiving bytes from a socket,
    and providing a friendly recv interface for GUI module.
    '''

    def __init__(self, fd):
        self.fd = fd
        self.sel = selectors.DefaultSelector()
        self.sel.register(self.fd, selectors.EVENT_READ)

    def recv(self):
        while True:
            self.sel.select()
            # if lock.acquire():
            byte = self.fd.recv(BUFSIZE)
            que.put(byte.decode('utf-8'))
            #    lock.release()
            # else:
            #    pass

    def __call__(self):
        self.recv()


class Connt(object):
    '''
    This module deals with establishing a TCP connection with host.
    '''

    def __init__(self, IP):
        self.HOST = IP
        self.PORT = 21567
        self.ADDR = (self.HOST, self.PORT)
        self.tcpCliSock = socket(AF_INET, SOCK_STREAM)

    def connect(self):
        self.tcpCliSock.connect(self.ADDR)

    def __call__(self):
        self.connect()


def main():
    gui = GUI()
    gui.root.mainloop()


if __name__ == '__main__':
    main()
